This is a simple project for SwiftUI, Core Data and CloudKit. In part 1, I will create a simple project in SwiftUI, then turn it into Core Data. In part 2, I will use CloudKit to sync with Core Data.
SwiftUI
Create a macOS SwiftUI project.
//
// ContentView.swift
// Todo List
//
// Created by zhaoxin on 2022/5/7.
//
import SwiftUI
struct ContentView: View {
@State private var items = [Item]()
@State private var addNewItem = false
let addNewItemPublisher = NotificationCenter.default.publisher(for: AddItemView.addNewItem)
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: true) {
if items.isEmpty {
Text("No Data")
} else {
List($items) { item in
HStack {
Text(DateFormatter.localizedString(from: item.startDate.wrappedValue, dateStyle: .none, timeStyle: .short))
Text(item.title.wrappedValue)
}
}
.frame(minWidth: 580, minHeight: 400, idealHeight: 600)
}
}
.onReceive(addNewItemPublisher) { notification in
if let userInfo = notification.userInfo as? [String:Item], let item = userInfo["new item"] {
items.append(item)
print(items)
}
}
Button {
add()
} label: {
Text("Add")
}
.sheet(isPresented: $addNewItem) {
AddItemView()
}
}
.padding()
.frame(width: 600, height: 600, alignment: .center)
}
private func add() {
addNewItem = true
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
// AddItemView.swift
// Todo List
//
// Created by zhaoxin on 2022/5/7.
//
import SwiftUI
struct AddItemView: View {
static let addNewItem = Notification.Name("addNewItem")
@Environment(\.dismiss) private var dismiss
@State private var item = Item(startDate: Date(), title: "")
var body: some View {
VStack {
Text("Add New Item")
.font(.title2)
TextField("What to do?", text: $item.title, prompt: Text("Go shopping."))
DatePicker("When?", selection: $item.startDate)
HStack {
Button {
save()
} label: {
Text("Save")
}
.disabled(item.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
Spacer()
Button {
cancel()
} label: {
Text("Cancel")
}
}
}
.padding()
}
private func save() {
NotificationCenter.default.post(name: AddItemView.addNewItem, object: nil, userInfo: ["new item" : item])
dismiss()
}
private func cancel() {
dismiss()
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
//
// Model.swift
// Todo List
//
// Created by zhaoxin on 2022/5/7.
//
import Foundation
struct Item:Identifiable {
let id = UUID().uuidString
var startDate:Date
var title:String
}
class ItemProvider:ObservableObject {
@Published var items = [Item]()
}
You can test and run the app.
Core Data Basics
NSPersistentContainer
NSPersistentContainer handles the creation of the Core Data stack and offers access to the NSManagedObjectContext as well as a number of convenience methods.
NSManagedObjectModel
The NSManagedObjectModel instance describes the data that is going to be accessed by the Core Data stack. During the creation of the Core Data stack, the NSManagedObjectModel is loaded into memory as the first step in the creation of the stack.
NSPersistentStoreCoordinator
The NSPersistentStoreCoordinator sits in the middle of the Core Data stack. The coordinator is responsible for realizing instances of entities that are defined inside of the model. It creates new instances of the entities in the model, and it retrieves existing instances from a persistent store (NSPersistentStore).
NSManagedObjectContext
The managed object context (NSManagedObjectContext) is the object that your application will interact with the most, and therefore it is the one that is exposed to the rest of your application. Think of the managed object context as an intelligent scratch pad. When you fetch objects from a persistent store, you bring temporary copies onto the scratch pad where they form an object graph (or a collection of object graphs). You can then modify those objects however you like. Unless you actually save those changes, however, the persistent store remains unaltered.
All managed objects must be registered with a managed object context. You use the context to add objects to the object graph and remove objects from the object graph. The context tracks the changes you make, both to individual objects’ attributes and to the relationships between objects. By tracking changes, the context is able to provide undo and redo support for you. It also ensures that if you change relationships between objects, the integrity of the object graph is maintained.
If you choose to save the changes you have made, the context ensures that your objects are in a valid state. If they are, the changes are written to the persistent store (or stores), new records are added for objects you created, and records are removed for objects you deleted.
Without Core Data, you have to write methods to support archiving and unarchiving of data, to keep track of model objects, and to interact with an undo manager to support undo. In the Core Data framework, most of this functionality is provided for you automatically, primarily through the managed object context.
Core Data
To use core data, first we must remove "Model.swift" file. Then we created a Core Data Model file, named it as "Model.xcdatamodeld".
Then we create it attributes.
Create ItemProvider
Add a new file, name it as "ItemProvider.swift".
//
// ItemProvider.swift
// Todo List
//
// Created by zhaoxin on 2022/5/7.
//
import Foundation
import CoreData
import AppKit
class ItemProvider:ObservableObject {
let container = NSPersistentContainer(name: "Model")
init() {
container.loadPersistentStores { description, error in
if let error = error {
let alert = NSAlert(error: error)
NSSound.beep()
alert.runModal()
}
}
}
}
We must first create the
NSPersistentContainer
, or the app will not know how to create an instance of "Item".
Core Data in SwiftUI
We need to set managedObjectContext
in environment
, so it will be use in the SwiftUI app.
//
// Todo_ListApp.swift
// Todo List
//
// Created by zhaoxin on 2022/5/7.
//
/*
<a href="https://www.flaticon.com/free-icons/reminder" title="reminder icons">Reminder icons created by max.icons - Flaticon</a>
*/
import SwiftUI
@main
struct Todo_ListApp: App {
@StateObject private var itemProvider = ItemProvider()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, itemProvider.container.viewContext)
}
}
}
Then in ContentView.swift
, we fetch the results from Core Data.
//
// ContentView.swift
// Todo List
//
// Created by zhaoxin on 2022/5/7.
//
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(sortDescriptors: [SortDescriptor(\.startDate, order: .forward)]) var items:FetchedResults<Item>
@State private var item:Item?
var body: some View {
VStack {
ScrollView(.vertical, showsIndicators: true) {
if items.isEmpty {
Text("No Data")
} else {
List(items) { item in
HStack {
Text(DateFormatter.localizedString(from: item.startDate!, dateStyle: .none, timeStyle: .short))
Text(item.title!)
}
}
.frame(minWidth: 580, minHeight: 400, idealHeight: 600)
}
}
Button {
add()
} label: {
Text("Add")
}
.sheet(item: $item) { item in
AddItemView(item: item)
}
}
.padding()
.frame(width: 600, height: 600, alignment: .center)
}
private func add() {
let item = Item(context: managedObjectContext)
item.id = UUID()
item.startDate = Date()
self.item = item
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In AddItemView
, the similar.
//
// AddItemView.swift
// Todo List
//
// Created by zhaoxin on 2022/5/7.
//
import SwiftUI
import CoreData
struct AddItemView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@Environment(\.dismiss) private var dismiss
@State var item:Item
var body: some View {
VStack {
Text("Add New Item")
.font(.title2)
TextField("What to do?", text: Binding($item.title) ?? .constant(""), prompt: Text("Go shopping."))
DatePicker("When?", selection: Binding($item.startDate) ?? .constant(Date()))
HStack {
Button {
save()
} label: {
Text("Save")
}
Spacer()
Button {
cancel()
} label: {
Text("Cancel")
}
}
}
.padding()
}
private func save() {
if var title = item.title {
title = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !title.isEmpty else {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Title is empty!", comment: "")
alert.alertStyle = .warning
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
NSSound.beep()
alert.runModal()
return
}
do {
item.title = title
try managedObjectContext.save()
} catch {
let alert = NSAlert(error: error)
NSSound.beep()
alert.runModal()
}
}
dismiss()
}
private func cancel() {
managedObjectContext.rollback()
dismiss()
}
}
In SwiftUI, we use "NotificationCenter" to post a notification in
save()
function. In Core Data, we no longer need the notification as we could save directly and the change will automatically trigger theFetchedResults
.